# coding=utf-8

# built in modules
import http.server
import hashlib
import cgi
import time
import urllib
import struct
import http.cookies
import binascii
import re
# TPB2 modules
import database
import utils

def size_format(bytes):
    levels = ["Bytes", "KiB", "MiB", "GiB", "TiB", "PiB"]
    result = bytes
    level = 0
    while result > 1024:
        result /= 1024
        level += 1
        if level == len(levels) - 1:
            break;
    return "%.2f %s"%(result, levels[level])

class HTMLPage:
    def __init__(self, wfile, title, parameters={}):
        self.tabs = ''
        self.wfile = wfile
        
        self.write('<!DOCTYPE html><html lang="en">')
        self.write("<head>", push_tab=True)
        self.write('<meta charset="utf-8">')
        self.write('<link rel="stylesheet" href="layout.css">')
        self.write('<title>%s</title>'%title)
        self.write('</head>', pop_tab=True)
        self.write('<body>', push_tab=True)

        self.write("<header>", push_tab=True)
        self.write('<img src="tpblogo_sm_ny.gif" alt="logo">')
        self.write_menu()
        self.write_search_form(parameters)
        self.write("</header>", pop_tab=True)

    def write_menu(self):
        self.write("<nav>", push_tab=True)
        self.write('<a href="/browse"><strong>Browse Entries</strong></a>')
        self.write("|")
        self.write('<a href="/peers">Peers</a>')
        self.write("|")
        self.write('<a href="/writeentry">New Entry</a>')
        self.write("|")
        self.write('<a href="/top">Top 100</a>')
        self.write("</nav>", pop_tab=True)
    
    def write_search_form(self, parameters):
        if "needle" in parameters:
            old_needle = parameters["needle"][0]
        else:
            old_needle = ""
        
        self.write('<form action="/search" method="GET">', push_tab=True)
        self.write('<input type="text" name="needle" value="%s">'%old_needle)
        self.write('<input type="submit" value="Search">')
        self.write("<fieldset>", push_tab=True)

        for category in sorted(utils.main_categories):
            self.write("<label>", push_tab=True)
            if category in parameters and parameters[category][0] == "on":
                checked = "checked"
            else:
                checked = ""
            self.write('<input type="checkbox" name="%s" %s>'
                       %(cgi.escape(category), checked))
            self.write("%s</label>"%cgi.escape(category), pop_tab=True)
        self.write("</fieldset>", pop_tab=True)
        self.write("</form>", pop_tab=True)
    
    def push_tab(self):
        self.tabs += "\t"
    
    def pop_tab(self):
        self.tabs = self.tabs[:-1]
    
    def write(self, data, push_tab=False, pop_tab=False):
        if pop_tab: 
            self.pop_tab()
        self.wfile.write(self.tabs.encode("utf-8"))
        self.wfile.write(data.encode("utf-8"))
        self.wfile.write('\n'.encode("utf-8"))
        if push_tab: 
            self.push_tab()
    
    def finish(self):
        self.write("</body>", pop_tab=True)
        self.write("</html>")

class TPB2UIRequestHandler(http.server.SimpleHTTPRequestHandler):
    def valid_resources(self):
        return {'/newentry': {"method": self.action_newentry, "type": "action"},
                '/newpeer':  {"method": self.action_newpeer,  "type": "action"},
                '/login':    {"method": self.action_login,    "type": "action"},
                '/vote':     {"method": self.action_vote,     "type": "action"},
                '/':           {"method": self.page_search,     "type": "page"},
                '/browse':     {"method": self.page_search,     "type": "page"},
                '/search':     {"method": self.page_search,     "type": "page"},
                '/top':        {"method": self.page_search,     "type": "page"},
                '/writeentry': {"method": self.page_writeentry, "type": "page"},
                '/entry':      {"method": self.page_readentry,  "type": "page"},
                '/lobby':      {"method": self.page_lobby,      "type": "page"},
                '/peers':      {"method": self.page_peers,      "type": "page"},
                '/layout.css':        {"type": "file"},
                '/tpblogo_sm_ny.gif': {"type": "file"},
                '/icon-magnet.gif':   {"type": "file"}}
    
    def is_logged_in(self, db):
        cookie = http.cookies.SimpleCookie()
        if(self.headers.get('Cookie')):
            cookie.load(self.headers.get('Cookie'))
        else:
            return False
        if not "password" in cookie:
            return False
        
        password, = db.execute("SELECT value FROM misc "+
                               "WHERE name='password'").fetchone()
        return cookie["password"].value == password
    
    def do_POST(self):
        length = int(self.headers.get('content-length'))
        ctype, pdict = cgi.parse_header(self.headers.get('content-type'))
        if ctype == "multipart/form-data":
            query = cgi.parse_multipart(self.rfile, pdict)
        elif ctype == "application/x-www-form-urlencoded":
            qs = self.rfile.read(length)
            query = urllib.parse.parse_qs(qs.decode("utf-8"),
                                          keep_blank_values = True)
        else:
            print("bad request: ctype == " + ctype)
            self.send_error(400, "'%s' is not a supported conent-type"%ctype)
            return
        
        if not self.path in self.valid_resources():
            self.send_error(404)
            return
        if self.valid_resources()[self.path]["type"] != "action":
            self.send_error(405)
            return
        db = database.get()
        logged_in = self.is_logged_in(db)
        if not logged_in and self.path != '/login':
            self.send_response(303)
            self.send_header("Location", "/lobby")
            self.end_headers()
        else:
            self.valid_resources()[self.path]["method"](query, db)
        db.close()
    
    def parameters_is_set(self, names, parameters):
        for name in names:
            if name in parameters:
                continue
            print("bad request: %s has no parameter '%s'"%
                  (repr(parameters), name))
            self.send_error(400, "The '%s' parameter is missing"%name)
            return False
        return True
    
    def parse_magnet(self, raw):
        utils.validate_magnet(raw)
        return raw
    
    def parse_category(self, raw):
        if not raw.isnumeric():
            raise ValueError("Category id not numeric")
        category = int(raw)
        if not category in utils.categories.values():
            raise ValueError("Category does not exist")
        return category
    
    def parse_size(self, raw):
        if not raw.isnumeric() or int(raw) < 0:
            raise ValueError("Size not a positve number")
        return int(raw)
    
    def parse_num_files(self, raw):
        if not raw.isnumeric() or int(raw) < 1:
            raise ValueError("num_files must be a number and at "+
                             "least one")
        return int(raw)

    def parse_created(self, raw):
        mandatory = "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}"
        frac_seconds = re.match(mandatory+":[0-9]{2}.[0-9]+$", raw)
        just_seconds = re.match(mandatory+":[0-9]{2}$", raw)
        just_minutes = re.match(mandatory+"$", raw)
        if frac_seconds:
            (date_time_minutes, sep, seconds) = raw.rpartition(':')
            seconds = round(float(seconds), 6)
            created = "%s:%.6f"%(date_time_minutes,seconds)
            created = time.strptime(created, "%Y-%m-%dT%H:%M:%S.%f")
        elif just_seconds:
            created = time.strptime(raw, "%Y-%m-%dT%H:%M:%S")
        elif just_minutes:
            created = time.strptime(raw, "%Y-%m-%dT%H:%M")
        else:
            raise ValueError("Time created must be in valid "+
                             "HTML5 local date and time string")
        created = int(time.mktime(created))
        utils.validate_time(created)
        return created
    
    def parse_key_file(self, raw):
        key_file = raw.encode("utf-8")
        return utils.unserialize_key_pair(key_file)
    
    def action_newentry(self, query, db):
        required = ["magnet", "title", "category", "size", "num_files",
                    "created", "text", "key_file"]
        if not self.parameters_is_set(required, query):
            return
        
        try:
            self.parse_magnet(query["magnet"][0])
            title = query["title"][0]
            category = self.parse_category(query["category"][0])
            size = self.parse_size(query["size"][0])
            num_files = self.parse_num_files(query["num_files"][0])
            created = self.parse_created(query["created"][0])
            key_file = self.parse_key_file(query["key_file"][0])
            utils.generate_entry(magnet, title, category, size, num_files,
                                 created, text, key_file)
        except (ValueError, utils.ValidateDataError) as error:
            print("parameter error: {0}".format(error))
            self.send_error(400, "parameter error")
            return
        
        self.send_response(303)
        self.send_header("Location", "/")
        self.end_headers()

    def action_newpeer(self, query, db):
        if not self.parameters_is_set(["ip", "port"], query):
            return

        ip = query["ip"][0]
        ip = list(map(int, ip.split(".")))
        ip = struct.pack("BBBB", ip[0], ip[1], ip[2], ip[3])
        
        db.execute("INSERT OR IGNORE INTO peers (ip, port, last_seen) "+
                   "VALUES (?, ?, ?)", (
                   ip, 
                   int(query["port"][0]), 
                   -1 ))
        self.send_response(303)
        self.send_header("Location", "/peers")
        self.end_headers()
    
    def action_login(self, query, db):
        if not self.parameters_is_set(["password"], query):
            return
        password, = db.execute("SELECT value FROM misc "+
                               "WHERE name='password'").fetchone()
        if query["password"][0] == password:
            cookie = http.cookies.SimpleCookie()
            cookie["password"] = query["password"][0]
            self.send_response(303)
            self.wfile.write((cookie.output()+"\r\n").encode("utf-8"))
            self.send_header("Location", "/")
            self.end_headers()
        else:
            self.send_response(303)
            self.send_header("Location", "/lobby")
            self.end_headers()
    
    def id_in(self, db):
        rows = db.execute("SELECT id FROM entries WHERE id = ?", (id,))
        if rows.fetchone() == None:
            print("bad request: " + repr(id) + " is not an entry id")
            self.send_error(403, 'Entry %d not found'%id)
            return
    
    def parse_vote(self, raw):
        if raw_vote == "+":
            return 1
        elif raw_vote == "-":
            return -1
        else:
            print("bad request: %s is neither '+' nor '-'"%repr(raw))
            self.send_error(400, "The 'vote' parameter must be "+
                            "'+' or '-', not '%s'"%raw)
            return False
    
    def action_vote(self, query, db):
        if not self.parameters_is_set(["entry", "vote"], query):
            return
        id = int(query["entry"][0])
        if not id_in(db):
            return
        vote = self.parse_vote(query["vote"][0])
        if not vote:
            return
        now = int(  time.mktime( time.gmtime() )  )
        db.execute("INSERT OR REPLACE INTO my_votes (entry, value, time) "+
                   "VALUES (?, ?, ?)", (id, vote, now))
        db.commit()
        self.send_response(303)
        self.send_header("Location", "/")
        self.end_headers()
    
    def parse_query(self):
        parts = self.query_string.split("&")
        query = {}
        for part in parts:
            key, sep, value = part.partition("=")
            if not key in query:
                query[key] = [value]
            else:
                query[key].append(value)
        return query
    
    def page_search(self, db):
        query = self.parse_query()
        if "needle" in query:
            needle = query["needle"][0]
        else:
            needle = False
        
        page = HTMLPage(self.wfile, "The Pirate Bay 2 --- Search results",
                        query)
        page.write("<section>", push_tab=True)
        if needle:
            page.write("<h1>Search results: %s</h1>"%(cgi.escape(needle),))
        else:
            page.write("<h1>Top 100</h1>")
        page.write("<table>", push_tab=True)
        page.write("<tr><th>Entry</th><th>Score</th><th>Vote</th></tr>")

        main_select = ("SELECT "+
                       "entries.id AS entry_id, "+
                       "entries.hash AS hash, "+
                       "my_votes.value AS my_vote, "+
                       "entries_info.title AS title, "+
                       "entries_info.created AS created, "+
                       "entries_info.rating AS rating "+
                       "FROM entries "+
                       "LEFT OUTER JOIN my_votes "+
                       "ON my_votes.entry = entries.id "+
                       "LEFT OUTER JOIN entries_info "+
                       "ON entries_info.entry = entries.id ")
        selects = []
        binds = []
        for category in utils.main_categories:
            if not category in query:
                continue
            if query[category][0] != "on":
                continue
            if needle:
                selects.append(main_select+
                               "INNER JOIN entries_fts_%s_all "%(category,)+
                               "ON entries.id == "+
                               "entries_fts_%s_all.docid "%(category,)+
                               "WHERE "+
                               "entries_fts_%s_all.title MATCH ? "%(category,))
                binds.append(needle)
            else:
                cats = utils.get_categories_from_main(category)
                in_expr = []
                for cat in cats:
                    in_expr.append("?")
                    binds.append(cat)
                in_expr = ", ".join(in_expr)
                selects.append(main_select+
                               "WHERE entries_info.category IN (%s)"%in_expr)
        
        if len(selects) == 0:
            if needle:
                rows = db.execute(main_select+
                                  "WHERE entries_fts_all.title MATCH ? "+
                                  "ORDER BY rating DESC "+
                                  "LIMIT 100",
                                  (needle,))
            else:
                rows = db.execute(main_select+
                                  "ORDER BY rating DESC "+
                                  "LIMIT 100")
        else:
            rows = db.execute("UNION ALL ".join(selects)+
                              "ORDER BY rating DESC "+
                              "LIMIT 100",
                              tuple(binds))
        for id, hash, my_vote, title, created, rating in rows:
            ready_title = cgi.escape(title)
            ready_time = time.strftime("%Y-%m-%d", time.gmtime(created))
            hexhash = binascii.hexlify(hash).decode("utf-8")
            
            page.write("<tr>", push_tab=True)
            page.write("<td>", push_tab=True)
            page.write('<p><a href="/entry?id=%s">%s</a></p>'%
                       (hexhash,ready_title))
            page.write('<p class="description">First seen %s</p>'%ready_time)
            page.write("</td>", pop_tab=True)
            if not rating:
                page.write('<td class="numbers">N/A</td>')
            else:
                page.write('<td class="numbers">%.2f</td>'%rating)
            page.write('<td class="vote-column">', push_tab=True)
            self.fragment_vote_form(page, id, my_vote)
            page.write("</td>", pop_tab=True)
            page.write("</tr>", pop_tab=True)
        page.write("</table>", pop_tab=True)
        page.write('</section>', pop_tab=True)
        
        page.finish()
    
    def do_GET(self):
        self.path, sep, self.query_string = self.path.partition("?")
        if not self.path in self.valid_resources():
            self.send_error(404)
            return

        if self.valid_resources()[self.path]["type"] == "action":
            self.send_error(405)
            return

        if self.valid_resources()[self.path]["type"] == "file":
            return http.server.SimpleHTTPRequestHandler.do_GET(self)

        db = database.get()

        if self.path != '/lobby' and not self.is_logged_in(db):
            self.send_response(303)
            self.send_header("Location", '/lobby')
            self.end_headers()
            db.close()
            return

        self.send_response(200)
        self.send_header('Content-type', 'text/html;charset=utf-8')
        self.end_headers()
        
        self.valid_resources()[self.path]["method"](db)
        db.close()

    def page_lobby(self, db):
        page = HTMLPage(self.wfile, "The Pirate Bay 2")
        
        page.write("<section>", push_tab=True)
        page.write("<h1>Please Login</h1>")
        page.write('<form action="/login" method="POST">', push_tab=True)
        page.write("<p>", push_tab=True)
        page.write('<label for="username">Username:</label>')
        page.write('<input type="text" name="username" value="root" disabled>')
        page.write("</p>", pop_tab=True)
        page.write("<p>", push_tab=True)
        page.write('<label for="password">Password:</label>')
        page.write('<input type="password" name="password">')
        page.write("</p>", pop_tab=True)
        page.write('<input type="submit" value="Log in">')
        page.write('</form>', pop_tab=True)
        page.write("</section>", pop_tab=True)
        
        page.finish()

    def page_writeentry(self, db):
        page = HTMLPage(self.wfile, "The Pirate Bay 2 --- New Entry")
        page.write("<section>", push_tab=True)
        page.write("<h1>New Entry</h1>")
        page.write("<article>", push_tab=True)
        page.write("<h1>Write a new entry</h1>")
        page.write('<form action="/newentry" method="POST">', push_tab=True)
        page.write("<p>", push_tab=True)
        page.write('<label for="magnet">Magnet URI:</label>')
        page.write('<input type="text" name="magnet" value="magnet:">')
        page.write("</p>", pop_tab=True)
        page.write("<p>", push_tab=True)
        page.write('<label for="title">Title:</label>')
        page.write('<input type="text" name="title">')
        page.write("</p>", pop_tab=True)
        page.write("<p>", push_tab=True)
        page.write('<label for="category">Category:</label>')
        page.write('<select name="category">', push_tab=True)
        for main_category in sorted(utils.main_categories):
            page.write('<optgroup label="%s">'%cgi.escape(main_category),
                       push_tab=True)
            for category in utils.categories:
                if utils.categories2main[category] != main_category:
                    continue
                page.write('<option value="%d">%s</option>'%
                           (utils.categories[category], cgi.escape(category)))
            page.write("</optgroup>", pop_tab=True)
        page.write("</select>", pop_tab=True)
        page.write("</p>", pop_tab=True)
        page.write("<p>", push_tab=True)
        page.write('<label for="size">Size (Bytes):</label>')
        page.write('<input type="number" name="size" min="0">')
        page.write("</p>", pop_tab=True)
        page.write("<p>", push_tab=True)
        page.write('<label for="num_files">Number of files:</label>')
        page.write('<input type="number" name="num_files" value="1" min="1">')
        page.write("</p>", pop_tab=True)
        page.write("<p>", push_tab=True)
        page.write('<label for="created">Creation time:</label>')
        now = time.strftime("%Y-%m-%dT%H:%M", time.gmtime())
        page.write('<input type="datetime-local" name="created" value="%s">'%
                   now)
        page.write("</p>", pop_tab=True)
        page.write("<p>", push_tab=True)
        page.write('<label for="text">Description:</label>')
        page.write('<textarea name="text"></textarea>')
        page.write("</p>", pop_tab=True)
        page.write("<p>", push_tab=True)
        page.write('<label for="key_file">Your private key file:</label>')
        page.write('<input type="file" name="key_file">')
        page.write("</p>", pop_tab=True)
        page.write('<p><input type="submit" value="Save"></p>')
        page.write('</form>', pop_tab=True)
        page.write("</article>", pop_tab=True)
        page.write('</section>', pop_tab=True)
        page.finish()

    def page_readentry(self, db):
        page = HTMLPage(self.wfile, "The Pirate Bay 2 --- View Entry")

        parts = self.query_string.split("&")
        parameters = {}
        for part in parts:
            key, sep, value = part.partition("=")
            parameters[key] = value
        if not "id" in parameters:
            page.write("<p>No entry id specified.</p>")
            page.finish()
            return
        hash = binascii.unhexlify(parameters["id"].encode("utf-8"))
        
        fields = ["magnet", "title", "creator", "text", "category", "size",
                  "num_files", "created", "author", "author_rating", "rating"]
        
        fields_sql = []
        for field in fields:
            fields_sql.append("entries_info."+field+" AS "+field)
        sql = ("SELECT "+", ".join(fields_sql)+" "+
               "FROM entries_info INNER JOIN entries "+
               "ON entries_info.entry = entries.id WHERE entries.hash = ?")
        r = db.execute(sql, (hash,))
        row = r.fetchone()
        if row == None:
            page.write("<p>There is no entry with id %s.</p>"%
                       cgi.escape(parameters["id"]))
            page.finish()
            return
        raw = {}
        for index, field in enumerate(fields):
            raw[field] = row[index]
        
        magnet = cgi.escape(raw["magnet"])
        title = cgi.escape(raw["title"])
        creator = cgi.escape(raw["creator"])
        text = cgi.escape(raw["text"])
        category = cgi.escape(utils.get_category_str(raw["category"]))
        size = size_format(raw["size"])
        num_files = raw["num_files"]
        created = time.strftime("%Y-%m-%dT%H:%M", time.gmtime(raw["created"]))
        created_readable = time.strftime("%Y-%m-%d %H:%M UTC",
                                         time.gmtime(raw["created"]))
        author = binascii.hexlify(raw["author"]).decode("utf-8")
        author_rating = raw["author_rating"]
        rating = raw["rating"]

        page.write("<section>", push_tab=True)
        page.write("<h1>View Entry</h1>")
        page.write("<article>", push_tab=True)
        page.write("<h1>%s</h1>"%title)
        page.write("<div>", push_tab=True)
        page.write('<dl class="cola">', push_tab=True)
        page.write("<dt>Type:</dt><dd>%s</dd>"%category)
        page.write("<dt>Files:</dt><dd>%d</dd>"%num_files)
        page.write("<dt>Size:</dt><dd>%s</dd>"%size)
        page.write("</dl>", pop_tab=True)
        page.write('<dl class="colb">', push_tab=True)
        page.write("<dt>Rating:</dt><dd>%.2f</dd>"%rating)
        page.write('<dt>Created:</dt><dd><time datetime="%s">%s</time></dd>'%
                   (created, created_readable))
        page.write("<dt>By:</dt>")
        page.write("<dd>", push_tab=True)
        page.write('<a href="/creator?id=%s">%s</a> (%.2f)'%
                   (author, creator, author_rating))
        page.write("</dd>", pop_tab=True)
        page.write("</dl>", pop_tab=True)
        page.write('<p class="download">', push_tab=True)
        page.write('<a href="%s">'%magnet, push_tab=True)
        page.write('<img src="icon-magnet.gif" alt="magnet">MAGNET LINK')
        page.write("</a>", pop_tab=True)
        page.write("</p>", pop_tab=True)
        page.write("<pre>%s</pre>"%text)
        page.write("</div>", pop_tab=True)
        page.write("</article>", pop_tab=True)
        page.write("</section>", pop_tab=True)
        page.finish()

    def page_peers(self, db):
        page = HTMLPage(self.wfile, "The Pirate Bay 2 --- Peers")
        
        page.write("<section>", push_tab=True)
        page.write("<h1>Peer List</h1>")
        page.write("<table>", push_tab=True)
        page.write("<tr><th>Ip</th><th>Port</th><th>Last Seen</th></tr>")
        rows = db.execute("SELECT ip, port, last_seen FROM peers")
        for ip, port, last_seen in rows:
            if last_seen != -1:
                last_seen = time.strftime("%Y-%m-%d", time.gmtime(last_seen))
            else:
                last_seen = "never"
            page.write("<tr><td>%s</td><td>%d</td><td>%s</td></tr>"%(
                "%d.%d.%d.%d"%(ip[0], ip[1], ip[2], ip[3]), 
                port,
                last_seen))
        page.write("</table>", pop_tab=True)
        page.write('</section>', pop_tab=True)

        self.fragment_new_peer(page)

        page.finish()

    def fragment_vote_form(self, page, entry_id, current_vote):
        page.write('<form action="/vote" method="POST">', push_tab=True)
        page.write('<input type="hidden" name="entry" value="%d">'%entry_id)
        if current_vote == 1:
            disabled = " disabled"
        else:
            disabled = ""
        page.write('<input type="submit" name="vote" value="+"%s>'%
                    disabled)
        if current_vote == -1:
            disabled = " disabled"
        else:
            disabled = ""
        page.write('<input type="submit" name="vote" value="-"%s>'%
                    disabled)
        page.write("</form>", pop_tab=True)

    def fragment_new_peer(self, page):
        page.write("<section>", push_tab=True)
        page.write("<h1>New Peer</h1>")
        page.write("<article>", push_tab=True)
        page.write("<h1>Connect a new peer</h1>")
        page.write('<form action="/newpeer" method="POST">', push_tab=True)
        page.write("<p>", push_tab=True)
        page.write('<label for="ip">IP:</label>')
        page.write('<input type="text" name="ip">')
        page.write("</p>", pop_tab=True)
        page.write("<p>", push_tab=True)
        page.write('<label for="ip">Port:</label>')
        page.write('<input type="text" name="port">')
        page.write("</p>", pop_tab=True)
        page.write('<p><input type="submit" value="Add to connect queue"></p>')
        page.write('</form>', pop_tab=True)
        page.write("</article>", pop_tab=True)
        page.write('</section>', pop_tab=True)
        
def main():
    httpd = http.server.HTTPServer(('', 8081), TPB2UIRequestHandler)
    
    print("start")
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        pass
    httpd.server_close()
    print("stop")
    
if __name__ == '__main__':
    main()
